Previous Page TOC Next Page


23 — Windows Games

by Bill Hatfield

Do you remember where you were in the early eighties? I do. I was at the arcade. The arcade craze was at its height, and computer games were the coolest thing in the world. At least, they were the coolest thing in my world. I was addicted.

But my love of arcade games didn't end with getting the highest score on Kix or Zaxxon. I wanted to do more than play in these stark, magical, interactive worlds. I wanted to create them. My parents encouraged my obsession by buying me a TRS-80 Model 1 when I was 12. Before long, I had a complete Pac-Man clone running. Well, complete except for the ghosts. It was a pretty dull game without the ghosts. But when I tried to add a ghost, the game slowed down to a crawl. My problem was Basic. I loved the Basic programming language because it was fairly easy to use (much easier than, say, assembly language). But Basic didn't work so well as a game programming language.

Later, I wrote a Q-bert clone on my Commodore 64. He was beautiful. He hopped from one cube to the next and when he hopped in the wrong direction, he fell into the unfathomable nothingness at the bottom of my screen. But, once again, when I added the first bouncing bad guy, the whole thing slowed to a crawl—all because of Basic.

Then I went to college and learned all kinds of nifty things like C and Pascal and Ada. These were real languages, compiled languages with manly code. Never again would I have to return to the wimpy Basic of my youth. Or so I was told.

But Basic came back. And here I am writing games in it again. The difference is that Basic is better (and faster) now. Today's Basic, in its Visual Basic incarnation, is a surprisingly good gaming platform that strikes an ideal balance between development speed, broad capabilities, and execution speed. It has support for everything you need. And, although you probably won't see the next DOOM written in Visual Basic (at least not until Visual Basic is compiled), you can do some pretty amazing stuff with this language.

This chapter shows you some of that stuff and explains how you can take advantage of it, not only in killer games, but in educational and multimedia programs as well.

The Top Ten Visual Basic Gaming Features

I love David Letterman. Even after being out of college these past several years, I've never been able to shake the urge to stay up late, eat junk food, and watch Letterman. It's a perfect end to the day. So in honor of my favorite talk-show host, this chapter will be in the form of a top ten list.

Here, now, I have in my left hand, a copy of tonight's top ten list. Tonight's category, Top Ten Visual Basic Features for Windows Game Development...

10.

Win32 and DCI

9.

Visual Interface

8.

Quick Development Time

7.

Win32 sndPlaySound

6.

Timers

5.

Collision Detection

4.

Win32 API Joystick Commands

3.

The Media Control Interface (MCI)

2.

The PaintPicture Method and ImageList Control

1.

Real Objects

Win32 and DCI

Not too long ago, the phrase "Windows game development" would be considered by most serious gaming folks to be a contradiction in terms. Windows was just too slow. It tried to be all things to all people and provide a nice, easy uniform interface to a broad variety of hardware. Although this setup was very convenient for most applications, it was death to games. Games always try to squeeze the most performance out of whatever platform they run on, and Windows didn't give them much flexibility. As a result, Windows game development stayed on the Solitaire and Minesweeper level for the greater part of the life of Windows 3.0 and 3.1.

After Windows 3.1 had been on the market for some time, an interesting thing happened. Both Intel and Microsoft had been working, more or less independently, on different ways of accessing video hardware more directly. They finally came together and created Device Control Interface (DCI). This was an interface that Windows drivers, not Windows applications, could use to access the hardware more directly and therefore get better performance.

Out of DCI grew a new application level interface called WinG. WinG evaluates the hardware of the target environment when you first install an application, and then uses that evaluation to choose what methods are the fastest for managing information. WinG achieves its performance boost by integrating and relying heavily on DCI. WinG promises to bridge the gap between the waning Windows 3.1 and the new Windows 95 and Windows NT 32-bit environments because the same WinG API works in all three.

Because DCI is so incredibly powerful, Microsoft made it a standard part of the Windows API. Now that DCI is integrated with Win32, both Windows NT and Windows 95 can leverage it to give them, in many cases, near-DOS performance in a device-independent environment.

With WinG, Windows 95 also has backward compatibility with all those games and multimedia titles written for WinG on Windows 3.1. WinG doesn't provide any performance boost to Win32, however. In fact, behind the scenes, WinG ends up doing nothing more than making its own Win32 calls. So if your focus is Windows 95 and Windows NT applications, you don't need to worry about WinG. All the performance you need for almost any application is built into the system already.

Microsoft is dedicated to making Windows the killer game platform. With that in mind, they are working on something called the Windows Game SDK. It will be an API created specifically to support the efforts of the game developer. It will, of course, be built on top of the already powerful Win32/DCI interface and will become an integrated part of the operating system. Finally, the PC platform will have native game support!

The Visual Interface

Much has been made of Visual Basic's visual interface and how it empowers the developer. This interface, known as a Visual Development Environment (VDE), is simply an extension of the Windows paradigm. In Word Processing, for example, WYSIWYG allowed you to view your document, while you're creating it, as it will look when it is printed out. Likewise, visual development enables you to design an application and, at the same time, see how it'll look when the user runs it.

This capability is especially important when you're developing games and multimedia applications. For example, it's tedious to determine the exact X and Y location where a sprite should appear and then figure the difference to the next location, and so on. It is much easier to simply drop the sprite on the form and then note its coordinates and move it. This allows you to see the changes and how it should appear.

Quick Development Time

Tied closely with visual development is the by-product of quick development time. Before the reviews, before the side-by-side evaluations, usually-conservative corporate developers quickly picked up Visual Basic and put it to use right away. Why? Corporations today need quick results and flexibility to prepare for change in the future. Visual Basic offers both of these things.

The game programming environment is not all that different. If anything, the heat is simply turned up. Each game company wants to have the hottest games out there on the market today. Every day that the next-level product is in development is a day of tremendous sales lost.

At the same time, the advent of the CD-ROM as the standard gaming platform, replacing the cramped space of the cartridge and floppy disk has freed developers from worrying so much about storage space. The endless march of hardware toward faster and faster processors has relieved developers from having to worry so much about the speed of every single instruction. These trends, combined with the market pressure, have caused teams of game programmers who used to be very C and assembly language-oriented to look toward class libraries and high level tools in order to develop the games more quickly.

That is not to say that the next game of the year will be written in Visual Basic. Its interpreted environment is still too slow for the most cutting edge stuff. However, many in-house multimedia projects, as well as corporate and commercial release projects, are already using it to build kiosks, hypermedia lookup systems and, yes, even some games. As development time becomes a higher priority and the combined efforts of Windows and Visual Basic produce an increasingly viable platform, the trend toward Visual Basic will only grow stronger.

Win32 sndPlaySound

Good sound can't make a bad game good, but it can make a good game great and a great game excellent. Unfortunately, the only sound support Visual Basic offers is Beep. In order to play a sound effect, you must use Win32 and a function called sndPlaySound. This single function provides a great deal of flexibility and is all you'll need for most of your WAV playing needs.

The declaration for this function looks like this:

Declare Function sndPlaySound Lib "winmm" _

Alias "sndPlaySoundA" (ByVal lpszSoundName As String, _

ByVal uFlags As Long) As Long

Note

You can include WIN32API.TXT as a module in your application to automatically declare all the Win32 functions and to declare constants you might need when calling them.

The first thing the function does is search for an entry in the [sounds] section of the registry to see whether it has an entry matching lpszSoundName. If not, the function assumes that lpszSoundName is the name of a WAV file and begins looking for it on the hard drive. It looks in the current directory, the Windows and Windows\System directories, and then works through the path and the network drives. If it finds the file, it plays the file. If not, it plays the default system beep. You can also specify a path as part of lpszSoundName.

uFlags determines how the sound is played. The following constants are the most commonly specified. They are combined by adding the values together and sending the result in uFlags.

'  flag values for uFlags parameter

Public Const SND_SYNC = &H0

Public Const SND_ASYNC = &H1

Public Const SND_NODEFAULT = &H2

Public Const SND_MEMORY = &H4

Public Const SND_LOOP = &H8

Public Const SND_NOSTOP = &H10

SND_SYNC, which is the default, causes the function to wait until the sound is done before returning control. SND_ASYNC is the opposite—it requests that control be returned to the calling function as soon as possible. Most games will have you using SND_ASYNC everywhere.

SND_NODEFAULT requests that the function do nothing if it cannot find the associated file. This request stops Visual Basic from playing the default beep. SND_MEMORY indicates that lpszSoundName is a pointer to an image of a WAV file already in memory.

SND_LOOP allows you to play a WAV file over and over again. You must also specify SND_ASYNC when you specify SND_LOOP. When you want the WAV file to stop playing, you just call sndPlaySound again with lpszSoundName set to the empty string (""). By the way, you can call sndPlaySound with NULL for the lpszSoundName anytime you want to stop a playing sound.

SND_NOSTOP indicates that you don't want to interrupt a currently playing sound, if there is one. If there is, the sndPlaySound function will quit and return False. SND_NOSTOP helps to prevent you from interrupting important sounds with less important ones.

The following lines of code demonstrate how these arguments work together:

rc = sndPlaySound("c:\windows\media\chimes.wav", SND_ASYNC)

The above line plays the WAV file in the specified directory and returns control to the calling function without waiting for the sound to finish playing.

rc = sndPlaySound("pretty.wav", SND_ASYNC + SND_LOOP)

The above line plays the specified WAV file again and again, without waiting for the sound to finish playing before returning control to the sndPlaySound function. Remember that you always have to use SND_ASYNC with SND_LOOP.

rc = sndPlaySound("",SND_ASYNC)

The above line stops the current WAV file from playing.

Timers

Timers are probably the simplest control on the Visual Basic palette. You just set the interval and the timer executes the code just that often. For their simplicity, timers are amazingly broad in their application.

The first and most obvious use for timers in games is for the main game loop. Games traditionally work by looping and doing things at (very quick) regular intervals. The code must move the sprites, check for collisions among the sprites, verify game parameters (to see whether the score should increase or the game should end) and update the screen all in a fraction of a second. That's the challenge of game programming. With timers, you can meet this challenge.

Timers are useful in other ways, too. To pause a game, just set the Enabled attribute of the timer for the main loop to False. To monitor the status of a device or anything else in your application, just set the timer's interval to check the device or status at whatever interval you like and write the code to do it. You can even use timers to animate an application's icon when the application is minimized.

Collision Detection

Okay, I admit, this isn't really a Visual Basic feature, but if you are going to implement games in any environment, you have got to know about basic collision detection. Collision detection is the process of determining which sprites in your world have bumped into each other. If you have a dozen sprites on the screen, you don't have to go through too many permutations to figure out that you are talking about a lot of potential collisions. Even though the most important stuff in the game seems to happen when things collide, figuring out when these collisions occur isn't always easy. It isn't always quick either.

Suppose that your game has a spaceship with lots of odd bits sticking out here and there. How do you find out if any of its parts are overlapping with any of the parts of the equally oddly shaped ships of the bad guy? The best answer is you don't. You fake it. If you have a finite amount of time to check all of your potential collisions, you must simplify the problem. You do this with a bounding box.

A bounding box is a box that encircles your sprite and defines its place. If any other sprite's bounding box is found partially inside your sprite's bounding box, the two will be considered touching, even if no actual graphic parts are hitting each other. The reason for this is that it's much quicker and easier to tell whether one box is touching another box than it is to tell whether two graphic parts are touching.

If you're worried that by using a bounding box you're sacrificing accuracy for speed and simplicity, keep in mind that this bounding box technique is really quite accurate, and the number of times that it gives a false positive response is too low to worry about. One technique you can use to reduce the possibility of errors even further is to reduce the size of your bounding box. Make the box even smaller than the outer edges of your sprite. This reduction causes some false negatives (saying there is no collision, when there was a slight one), but usually you can find a balance so good that the user will never be able to tell the difference. In fact, here's a routine to do it:

Type ScreenObject

      X As Integer

      Y As Integer

      Height As Integer

      Width As Integer

End Type

Private Function Collision(Obj1 As ScreenObject, Obj2 As ScreenObject) As Integer

' These are variables for holding the X and Y position of

' the lower-right corners of the objects

Dim LowerX1 As Integer, LowerY1 As Integer

Dim LowerX2 As Integer, LowerY2 As Integer

Dim Collided As Integer

' Find the lower right corners of the objects

LowerX1 = Obj1.X + Obj1.Width - 1

LowerY1 = Obj1.Y + Obj1.Height - 1

LowerX2 = Obj2.X + Obj2.Width - 1

LowerY2 = Obj2.Y + Obj2.Height - 1

Collided = True

If ((LowerX1 < Obj2.X) Or (Obj1.X > LowerX2) Or _

     (LowerY2 < Obj2.Y) Or (Obj1.Y > LowerY2)) Then

        Collided = False

  End If

Collision = Collided

End Function

The function accepts two user-defined type variables that each give the X, Y, height, and width a sprite. The function returns a simple True or False indicating whether there has been a collision between the two. The function determines whether there has been a collision by assuming a collision has occurred and then setting out to disprove it. If the function cannot disprove it, then the collision must have happened. This backwards method turns out to be just as accurate and much faster than any other way you might approach the problem.

When you're writing your game, your initial inclination might be to just loop through all the sprites and check for collision with every other sprite. Even when using bounding boxes, this process can very quickly turn your application into a dog. To reduce the sheer number of collision tests that you have to do, ask yourself two questions for each collision:

  1. Is it possible?

  2. Do I care?

First, is it physically possible that this sprite that only runs across the top of my screen could run into the player who is constrained in the lower half of the screen? If not, then forget it! Don't waste your time checking things that couldn't possibly happen.

Second, does it matter if these two sprites ran into each other? Does anything really occur when an alien shot from one ship hits an alien shot from another ship? If you are not going to change anything if a collision occurs, you don't need to find out whether one has happened.

Win32 API Joystick Commands

Although Visual Basic doesn't have built-in commands that allow you to access the joystick, you do have full access to the Win32 joystick commands from Visual Basic. The commands you will use most often are joyGetDevCaps and joyGetPos, which are explained in the following sections.

joyGetDevCaps

You use joyGetDevCaps to get information about the joystick capabilities and calibration. When working with a gaming application, you will want to call joyGetDevCaps in your initialization code so that you have the information later when you need it.

The declaration for joyGetDevCaps is as follows:

Declare Function joyGetDevCaps Lib "winmm" Alias "joyGetDevCapsA" _

(ByVal id As Long, lpCaps As JOYCAPS, _

ByVal uSize As Long) As Long

Because some systems can have two joysticks connected, you must specify the ID number of the joystick that you want information on. The two constants that you can use are:

Global Const JOYSTICKID1 = 0

Global Const JOYSTICKID2 = 1

The second parameter in the declaration is used to pass a structure by reference that will receive the information from the external function. The third parameter is the size of the filled-in structure, in bytes. You can do a Len(), which sends your JOYCAPS variable to figure out what to send for uSize.

The structure's definition looks like the following:

Type JOYCAPS

   wMid As Long

   wPid As Long

   szPname As String * 25

   wXmin As Long

   wXmax As Long

   wYmin As Long

   wYmax As Long

   wZmin As Long

   wZmax As Long

   wNumButtons As Long

   wPeriodMin As Long

   wPeriod Max As Long

End Type

The information this structure gathers is as follows:


Note

The last two items of information may be confusing to you. Using other Win32 functions, you can capture the joystick to a particular window so that it sends events to that window when anything happens with the joystick. Although this setup may sound ideal, you can't do it from Visual Basic because you have no way of capturing the associated Windows events.

The most important bits of information here are the minimums and maximums on each of the different axes. You may be surprised to find the Z axis included because most gaming joysticks don't support Z-axis movement. This specification is, no doubt, anticipating future input devices that will have this movement.

When you ask a joystick where it is, it doesn't just tell you that it is being pulled right or left. It gives you a number. In order for that number to make any sense, you have to know what the maximum and minimum X and Y values that the joystick supplies are. Then you can make an estimate of how far left or right the joystick is being pulled and adjust the action in the program accordingly. You must call the joyGetDevCaps function at the beginning of your program so you'll have a context for understanding the numbers sent to you when you get information from the joystick.

The other bit of information that will be interesting to you is the number of buttons the joystick supports. Most joysticks today support at least two, and some support as many as three or four. You may want to give the user the flexibility of assigning meaning to any extra joystick buttons.

joyGetPos

You use joyGetPos to get the current position of the joystick. You will call joyGetPos again and again, probably as a part of the processing done in a timer to determine the current joystick position and update the action accordingly.

The declaration for joyGetPos looks like the following:

Declare Function joyGetPos Lib "winmm" Alias "joyGetPos" _

(ByVal id As Long, lpInfo As JOYINFO) As Long

This declaration receives a joystick identification number just as joyGetDevCaps did to identify the joystick for which you want position information. It also takes a user-defined type as an argument by reference to pass back the information you want.

JOYINFO looks like the following:

Type JOYINFO

   wXpos As Long

   wYpos As Long

   wZpos As Long

   wButtons As Long

End Type

JOYINFO gives you a number for each axis and then a long indicating which buttons are pressed. As I mentioned before, you can understand the number passed for each axis by looking at the minimum and maximum numbers for that axis, which you obtained at the beginning of the application.

How does JOYINFO pass all the button information in one long? The information is encoded in the bits. The first bit specifies the first button, the second bit specifies the second button, and so on. So if the first button was being pushed, you would receive 00000001, or a decimal 1, in the least significant byte of the long. If the first and third buttons were being pressed, you'd get 00000101 or a decimal 5. The easiest way to decode these numbers is with the constants provided:

Global Const JOY_BUTTON1 = &H0001

Global Const JOY_BUTTON2 = &H0002

Global Const JOY_BUTTON3 = &H0004

Global Const JOY_BUTTON4 = &H0008

All you have to do to find out about any particular button is enter the following code:

If (lJoyInfo.wButtons AND JOY_BUTTON1) Then

The AND in the above code does a bitwise AND with the mask and returns a value other than zero only if that bit is set.

The Media Control Interface (MCI)

The Media Control Interface is an easy-to-use interface to the many multimedia devices that are connected to your user's system. Visual Basic has a control that gives you a very flexible way of using the MCI. Win32 also has a command called mciSendString() that provides an English-like text interface to MCI. This command enables you to completely avoid all the device-specific details and simply make MCI work.

Chapter 9, "Multimedia," is completely devoted to covering the MCI control and the mciSendString() command. In addition, that chapter provides numerous examples of the language to get you started using it.

For game programming, you are most likely to use this interface to play background music in the form of MIDI files. So, before I leave this topic, let me show you a series of commands that will play MIDI files.

When you want to begin playing the music, execute these commands:

OPEN MYSONG.MID TYPE SEQUENCER ALIAS SEQ

PLAY SEQ FROM 0

These commands open a MIDI file called MYSONG and play it from the beginning. When you want to stop the music, execute the following command:

STOP SEQ

To start up the music again where you left off, use the following command:

PLAY SEQ

When you have stopped the music and are ready to close the application, be sure and close the MIDI file first with this command:

CLOSE SEQ

For more information on exactly how to send these strings and what the mciSendString() command does, see the chapter on multimedia (Chapter 9).

The PaintPicture Method and ImageList Control

The PaintPicture method and ImageList control are new with Visual Basic 4.0. They go a long way toward making your sprite creation and utilization as simple as possible. They are so important that I've devoted a complete chapter to them. See the Animation chapter (Chapter 10) for more information on these items and a really cool quote from Walt Disney.

Real Objects

It has been a long time coming, but finally Visual Basic is a truly object-oriented environment. That is, it allows you to create your own classes and then create new objects from those classes. The classes can have their own functions and data structures. The following sections explain how classes work and demonstrate how you can put them to work in a game.

How Classes Work

Choose Insert | Class Module in your application. A new code window appears on-screen, much as it does for a regular module. In it you can now declare functions and procedures as well as data structures, just as you would for a form. You will notice one object called Class and two methods for it: Initialize and Terminate. These methods are executed when your class is first instantiated (turned into an object) and when the object is destroyed. Once you create a class module, you can't simply call functions from it the way you do a normal module. You have to remember that a class is just a definition of an object. You have to create an actual object to work with before you can start calling its functions.

To create an object, you would use the following code:

Dim NewObject As New MyClass

This code assumes you have created a class module called MyClass. If you wanted to call a procedure called DoIt in MyClass, you would enter the following:

MyClass.DoIt

A class is a very flexible mechanism. Remember that because this is a class and you instantiate objects from it, you can create as many identical objects as you like. They will have identical functions, procedures and data structures, but each will have its own copy of data in the data structures.

If you are a hard-core Visual Basic person, you might regard this new feature as a minor expansion of capabilities, but don't gloss over this one lightly. Classes can have the power to significantly alter the way you design and develop your applications. In earlier versions of Visual Basic, you were forced to create either global- or form-level functions that did all the work of your application. Now you can segment your application functionality and data into components that make sense for your problem. This capability is especially useful in game programming.

A Gaming Example

Imagine that you want to create a Q-bert-style video game. In case you don't remember, Q-bert was a ball-shaped creature with legs that hopped around on cubes arranged in the form of a pyramid. When he landed on top of a cube, the cube would change color. When he changed the color of all the cubes, he got to go to the next level. Of course, there were meanies jumping around on the pyramid too who killed the hapless hero if he landed on the same cube with them.

The following might be a rough outline of the application's flow:

Query: Does anybody want to move?

Movement phase: Loop through the movement of all currently jumping pieces at once. Loop through the complete jump.

Collision Detection: Is the hero on the same cube as meanie? If so, kill the hero.

Did the hero land on a cube that hasn't already had its color changed? If so, change the color and add points.

Are all the cubes' colors changed on the board? If so, do the following:

Add points

Clear board

Set up next board

Display board

Place hero on board

Restart the meanies

Now that you have a rough idea of what the main loop of the program will look like, you are likely to start thinking in terms of procedures you need to write. Don't. Instead, think in terms of objects. What objects are involved with the application? The hero is an object. The meanies are objects. Even the game board itself is an object.

When you have a rough idea of what objects are involved, begin thinking about the objects. Each object has properties and methods. Properties are the data associated with that object that either may or may not be accessed by other objects (your choice). Methods are simply the procedures and functions in the object.

What kinds of things should the hero be able to do? He should be able to check user input to see whether he should jump from this cube to another. What kinds of data will he want to keep? He needs to monitor his X and Y location and which direction he is facing. The meanies have much the same data and procedures, except that instead of checking user input to decide to jump, they will check some clever little algorithm you have cooked up.

For the board, you might want a procedure that changes a cube's color, a function like IsLevelComplete, and a procedure that does everything necessary to take the user to the next level. You will also need an internal 2-D array that represents the game board and can be used to update the screen.

You begin to see how an application divides itself up among its objects. There are no hard and fast rules on how this is done; in fact, many volumes have been written on this topic with no clear agreement. The most important thing about the process of creating objects and segmenting your functionality is to remember that it is iterative. You will start with a rough idea and then shape it over time into your master application. Hopefully, the new object-oriented paradigm will also make your code more modular. With modular code, the next time you start a game that is similar to this one, you'll be able to drop in the appropriate objects and start off half-finished.

Summary

This chapter has been a wild romp through a broad variety of topics that are important to the Visual Basic game developer. Not only did we talk about animation and sound but also about collision detection and object-oriented design. There is a lot more to learn, but hopefully this foundation will serve you well.

Previous Page TOC Next Page